﻿#include "asycipc/sliding_window.hpp"

#include <stdexcept>

namespace asycipc {
namespace {
std::chrono::milliseconds ComputeBackoff(std::chrono::milliseconds base,
                                         uint32_t attempt) {
  const uint32_t shift = attempt < 6 ? attempt : 6;  // limit growth
  return base * (1u << shift);
}
}  // namespace

SlidingWindow::SlidingWindow(std::size_t window_size,
                             std::chrono::milliseconds base_retry,
                             uint32_t max_attempts)
    : window_size_(window_size),
      base_retry_(base_retry),
      max_attempts_(max_attempts) {
  if (window_size_ == 0) {
    throw std::invalid_argument("window_size must be >0");
  }
  if (base_retry_.count() <= 0) {
    throw std::invalid_argument("base_retry must be positive");
  }
  if (max_attempts_ == 0) {
    throw std::invalid_argument("max_attempts must be >0");
  }
}

void SlidingWindow::Push(const Frame& frame,
                         std::chrono::steady_clock::time_point now) {
  if (IsFull()) {
    throw std::runtime_error("sliding window full");
  }
  PendingEntry entry;
  entry.frame = frame;
  entry.next_retry = now + base_retry_;
  auto [it, inserted] = pending_.emplace(frame.seq_no, std::move(entry));
  if (!inserted) {
    throw std::runtime_error("duplicate sequence number");
  }
}

bool SlidingWindow::MarkAck(uint64_t seq) {
  auto it = pending_.find(seq);
  if (it == pending_.end()) {
    return false;
  }
  pending_.erase(it);
  return true;
}

bool SlidingWindow::MarkNack(uint64_t seq,
                             std::chrono::steady_clock::time_point now) {
  auto it = pending_.find(seq);
  if (it == pending_.end()) {
    return false;
  }
  it->second.next_retry = now;
  return true;
}

std::vector<SlidingWindow::RetryItem> SlidingWindow::CollectRetries(
    std::chrono::steady_clock::time_point now) {
  std::vector<RetryItem> due;
  for (auto it = pending_.begin(); it != pending_.end();) {
    auto& entry = it->second;
    if (entry.next_retry > now) {
      ++it;
      continue;
    }
    if (entry.attempts >= max_attempts_) {
      // Remove exhausted entry but do not retry further.
      it = pending_.erase(it);
      continue;
    }
    entry.attempts += 1;
    entry.next_retry = now + ComputeBackoff(base_retry_, entry.attempts - 1);
    due.push_back({entry.frame, entry.attempts});
    ++it;
  }
  return due;
}

}  // namespace asycipc
